using System;
using OhioState.AssetManaged;
using OhioState.Math;

namespace OhioState.Graphics
{	
    /// <summary>
	/// A camera model containing a matrix to transform from the world space to camera space with the z-axis coming out
    /// the screen, and a projection matrix indicating the field of view and any aspect ratio's.
	/// </summary>
    [Serializable()]
    public class Camera : SharedAsset,  ICamera
    {
        #region Public properties
        /// <summary>
        /// Get or set the aspect ratio for the camera.
        /// </summary>
        public float AspectRatio
        {
            get { return _aspectRatio; }
            set
            {
                setAspectRatio(value);
            }
        }
        #endregion

        #region Constructors
        /// <summary>
        /// Constructor similar to OpenGL's glFrustum call. Sets up the projection matrix.
        /// </summary>
        /// <param name="left">The left extent at the near plane for the camera frustum.</param>
        /// <param name="right">The right extent at the near plane for the camera frustum.</param>
        /// <param name="bottom">The bottom extent at the near plane for the camera frustum.</param>
        /// <param name="top">The top extent at the near plane for the camera frustum.</param>
        /// <param name="near">Distance to the near clipping plane. Must be greater
        /// than zero and less than <paramref name="far"/>.</param>
        /// <param name="far">Distance to the far clipping plane.</param>
		public Camera( float left, float right, float bottom, float top, float near, float far )
		{
			SetProjection( left, right, bottom, top, near, far );
            projectionLast = projection;
        }
        /// <summary>
        /// Constructs a new camera using a syntax similar to OpenGL's gluPerspective.
        /// </summary>
        /// <param name="viewAngle">Angle (in degrees) specifying the field of view.</param>
        /// <param name="aspectRatio">Height over width aspect ratio of the window.</param>
        /// <param name="near">Distance to the near clipping plane. Must be greater
        /// than zero and less than <paramref name="far"/>.</param>
        /// <param name="far">Distance to the far clipping plane.</param>
        public Camera( float viewAngle, float aspectRatio, float near, float far )
		{
			SetProjection( viewAngle, aspectRatio, near, far );
            projectionLast = projection;
        }
        /// <summary>
        /// Default constructor. A reasonable camera with a 90-degree field of view.
        /// </summary>
		public Camera()
		{
			SetProjection( -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 100.0f );
            projectionLast = projection;
        }
        #endregion

        #region Public methods
        /// <summary>
        /// Sets the projection matrix to an equivalent gluPerspective camera
        /// </summary>
        /// <param name="viewAngle">Angle (in degrees) specifying the field of view.</param>
        /// <param name="aspectRatio">Height over width aspect ratio of the window.</param>
        /// <param name="near">Distance to the near clipping plane. Must be greater
        /// than zero and less than <paramref name="far"/>.</param>
        /// <param name="far">Distance to the far clipping plane.</param>
        public void SetProjection( float viewAngle, float aspectRatio, float near, float far )
        {
            if (viewAngle <= 0.0f || viewAngle >= 180.0f)
                throw (new ArgumentOutOfRangeException("viewAngle",viewAngle,
                    "The view angle must be greater than zero and less than 180 degrees."));
            if (aspectRatio <= 0.0f)
                throw (new ArgumentOutOfRangeException("aspectRatio",aspectRatio,
                    "The aspect ration must be greater than zero."));
            if ((near <= 0.0f) || (near >= far))
                throw (new ArgumentOutOfRangeException("near",near,
                    "The near distance must be greater than zero and less than far."));

            float height = near * (float)System.Math.Tan(System.Math.PI * viewAngle / 180.0);
            float width = height * aspectRatio;
            SetProjection(-width, width, -height, height, near, far);
            FireUpdateChangingEvent(this);
        }
        /// <summary>
        /// Sets the projection matrix equivalantly to the glFrustum call.
        /// </summary>
        /// <param name="left">The left extent at the near plane for the camera frustum.</param>
        /// <param name="right">The right extent at the near plane for the camera frustum.</param>
        /// <param name="bottom">The bottom extent at the near plane for the camera frustum.</param>
        /// <param name="top">The top extent at the near plane for the camera frustum.</param>
        /// <param name="near">Distance to the near clipping plane. Must be greater
        /// than zero and less than <paramref name="far"/>.</param>
        /// <param name="far">Distance to the far clipping plane.</param>
        public void SetProjection( float left, float right, float bottom, float top, float near, float far )
		{
            projectionLast = projection;
            projection = new Matrix4();
            projection[0, 0] = 2.0f * near / (right - left);
			projection[0,2] = (right+left) / (right-left);
			projection[1,1] = 2.0f*near / (top-bottom);
			projection[1,2] = (top+bottom) / (top-bottom);
			projection[2,2] = -(far+near) / (far-near);
			projection[2,3] = -2.0f*far*near / (far-near);
			projection[3,2] = -1.0f;
			projection[3,3] = 0.0f;
            _aspectRatio = (right - left) / (top - bottom);
            FireUpdateChangingEvent(this);
        }
        /// <summary>
        /// Returns a copy of the model-view matrix.
        /// </summary>
        /// <returns>A Matrix4.</returns>
		public Matrix4 GetModelViewMatrix()
		{
			Matrix4 returnVal = 1.0f * this.viewing;
			return returnVal;
		}
        /// <summary>
        /// Sets the model-view matrix.
        /// </summary>
        /// <param name="matrix">A Matrix4.</param>
		public void SetModelViewMatrix( Matrix4 matrix )
		{
			viewing = 1.0f * matrix;
            FireUpdateChangingEvent(this);
        }
        /// <summary>
        /// Returns a copy of the projection matrix.
        /// </summary>
        /// <returns>A Matrix4.</returns>
        public Matrix4 GetProjectionMatrix()
		{
			Matrix4 returnVal = 1.0f * this.projection;
			return returnVal;
		}
        /// <summary>
        /// Sets the projection matrix.
        /// </summary>
        /// <param name="matrix">A Matrix4.</param>
        public void SetProjectionMatrix( Matrix4 matrix )
		{
			projection = 1.0f * matrix;
            FireUpdateChangingEvent(this);
        }
        #endregion

        #region IZoomLens Members
        /// <summary>
        /// Apply a zoom using the scale factor.
        /// </summary>
        /// <param name="zoom">A zoom scale factor. Less than one zooms in.
        /// Greater than one zooms out.</param>
        public virtual void ZoomLens( float zoom )
		{
			//
			// Apply a scale matrix to the projection matrix.
			//
			// The constructor to matrix provides an Identity matrix, so 
			// we just need to set the diagonal elements.
			//
			Matrix4 scale = new Matrix4();
			for(int i=0; i< 2; i++)
			{
				scale[i,i] = zoom;
			}
			//
			// The scale should happen in eye-space. This could either be the
			// last transformation of the modelView matrix or the first of the
			// projection matrix. We use the projection matrix here.
			//
			projection = projection * scale;
			//
			// Notify any subscribers that the projection matrix has changed.
			//
			FireUpdateChangingEvent(this);
		}
		#endregion

		#region ITransform Members
        /// <summary>
        /// Gets or sets the pivot point for any subsequent rotations.
        /// </summary>
        public Vector3 RotationCenter
        {
            get { return _rotationCenter; }
            set { _rotationCenter = value; }
        }
        /// <summary>
        /// Apply a rotation about the specificed axis.
        /// </summary>
        /// <param name="radians">The amount of rotation in radians.</param>
        /// <param name="axis">A <typeparamref name="Vector3"/> specifying the axis about which to rotate.</param>
        public virtual void Rotate(float radians, Vector3 axis)
		{
            Matrix4 translate = new Matrix4();
            translate[0, 3] = -_rotationCenter[0];
            translate[1, 3] = -_rotationCenter[1];
            translate[2, 3] = -_rotationCenter[2];
            viewing = translate * viewing;
            //
			// Rotate about the axis the specified radians amount
			//
			axis.normalize();
			Matrix4 rotation = new Matrix4( radians, axis );
			//
			// The rotation should happen in eye-space, so pre-multiply it.
			//
			viewing = rotation * viewing;
            translate[0, 3] = _rotationCenter[0];
            translate[1, 3] = _rotationCenter[1];
            translate[2, 3] = _rotationCenter[2];
            viewing = translate * viewing;
            //
			// Notify any subscribers that the ModelView matrix has changed.
			//
            FireUpdateChangingEvent(this);
		}
        /// <summary>
        /// Apply a translation.
        /// </summary>
        /// <param name="x">The x-axis displacement.</param>
        /// <param name="y">The y-axis displacement.</param>
        /// <param name="z">The z-axis displacement.</param>
        public virtual void Translate( float x, float y, float z )
		{
			//
			// Simple translate in the eye-space. This allows for both
			// camera pans, as well as free-form flight (moving the eye location).
			//
			Matrix4 translate = new Matrix4();
			translate[0,3] = x;
			translate[1,3] = y;
			translate[2,3] = z;
			//
			// The translation should happen in eye-space, so pre-multiply it.
			//
			viewing = translate * viewing;
			//
			// Notify any subscribers that the ModelView matrix has changed.
			//
            FireUpdateChangingEvent(this);
		}
        /// <summary>
        /// Apply a translation.
        /// </summary>
        /// <param name="translation">A translation vector.</param>
        public virtual void Translate( Vector3 translation )
		{
			//
			// A alternative interface using a Vec3f object.
			//
			Translate( translation.x,  translation.y, translation.z );
            FireUpdateChangingEvent(this);
		}
        /// <summary>
        /// 
        /// </summary>
        /// <param name="scaleX"></param>
        /// <param name="scaleY"></param>
        /// <param name="scaleZ"></param>
        void ITransform.Scale(float scaleX, float scaleY, float scaleZ)
        {
            throw new NotImplementedException();
        }
        #endregion

		#region IManipulator Members
        /// <summary>
        /// Prepares the manipulator for a sequence of actions.
        /// </summary>
        public override void BeginManipulation()
		{
			// TODO:  Add Camera.BeginManipulation implementation
			// This is usually called on Mouse-down or a similar event.
			// Push a new matrix stack to keep track of the incremental changes
			// to the current matrices.
			//
			viewingLast = viewing;
			projectionLast = projection;
		}
        /// <summary>
        /// Cleans up any state in the manipulator.
        /// </summary>
        public override void Reset()
		{
			// TODO:  Add Camera.Reset implementation
			viewing = viewingLast;
			projection = projectionLast;
		}
		#endregion

        #region Private and protected methods
        private void setAspectRatio(float aspectRatio)
        {
            if (aspectRatio <= 0.0f)
                throw (new ArgumentOutOfRangeException());
            //
            // Scale the y-axis of the projection matrix by the ratio between
            //    the old and the new aspects.
            //
            Matrix4 scale = new Matrix4();
            scale[1, 1] = aspectRatio / _aspectRatio;
            projectionLast = projection;
            projection = scale * projection;
            _aspectRatio = aspectRatio;
            FireUpdateChangingEvent(this);
        }
        #endregion

        #region Private members
        //
        // TODO: To make this thread safe, we will need a mechanism for only
        // allowing one client to manipulate the camera at a time. I think the
        // Synchronization attribute above does this, but it needs more research.
        //
        // Matrices for the current viewing and projection matrices.
        /// <summary>
        /// View Matrix
        /// </summary>
        protected Matrix4 viewing = new Matrix4();
        /// <summary>
        /// Projection Matrix
        /// </summary>
        protected Matrix4 projection = new Matrix4();
        /// <summary>
        /// View Matrix for the state before a manipulation began.
        /// </summary>
        protected Matrix4 viewingLast = new Matrix4();
        /// <summary>
        /// Projection Matrix for the state before a manipulation began.
        /// </summary>
        protected Matrix4 projectionLast = new Matrix4();
        // The camera aspect ration.
        private float _aspectRatio = 1.0f;
        // The rotation center used in the ITrackball interface.
        private Vector3 _rotationCenter = new Vector3(0.0f, 0.0f, 0.0f);
        #endregion
    }
}
